本篇文章內有:
讓我們回到程式碼的懷抱,先前所寫的 AWS CDK ,其實很多設定是可以變成參數,讓我們可以簡單新增類似,但是卻有點微妙地不同的 Stack 們。
首先,我們來把 cdk.StackProps
擴充一下:
interface AppStackProps extends cdk.StackProps {
readonly s3BucketExpiration?: cdk.Duration;
readonly ec2Arch?: "x86_64" | "arm64";
readonly lambdaValue?: string;
readonly lambdaIsReturn?: boolean;
}
就做幾個簡單的開關:
s3BucketExpiration
:控制 Amazon S3 儲存貯體的物件過期時間。ec2Arch
:使用 ADM64 或是 ARM64 平台。lambdaValue
:指定 AWS Lambda 函數使用的字串。lambdaIsReturn
:是否讓 AWS Lambda 函數回傳字串。再來,將擴充好的 AppStackProps
指定在 AppStack
的建構子中:
export class AppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: AppStackProps) {
super(scope, id, props);
這樣就完成讓 AppStack
可以接收外部的參數了,但是還沒結束,接收是一回事,要讓 AppStack
針對這些參數作動是一件事。
我們來把 s3BucketExpiration
指定給 Amazon S3 儲存貯體,並且將原本的 7 天做為預設值:
bucket.addLifecycleRule({
expiration: props?.s3BucketExpiration ?? cdk.Duration.days(7),
});
在這個例子中,我們只會在有明確指示的情況下,將 Amazon EC2 執行個體更換成 x86_64
:
new cdk.aws_ec2.Instance(this, "instance", {
vpc,
instanceType:
props?.ec2Arch === "x86_64"
? cdk.aws_ec2.InstanceType.of(
cdk.aws_ec2.InstanceClass.BURSTABLE2,
cdk.aws_ec2.InstanceSize.MICRO
)
: cdk.aws_ec2.InstanceType.of(
cdk.aws_ec2.InstanceClass.BURSTABLE4_GRAVITON,
cdk.aws_ec2.InstanceSize.SMALL
),
machineImage: new cdk.aws_ec2.AmazonLinuxImage({
generation: cdk.aws_ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
cpuType:
props?.ec2Arch === "x86_64"
? cdk.aws_ec2.AmazonLinuxCpuType.X86_64
: cdk.aws_ec2.AmazonLinuxCpuType.ARM_64,
}),
}).role.addManagedPolicy(
cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
"AmazonSSMManagedInstanceCore"
)
);
最後,我們將嵌入的程式碼做調整,直接在樣板字面值 (template literals or template strings) 中把變數帶入:
new cdk.aws_lambda.Function(this, "function", {
vpc,
runtime: cdk.aws_lambda.Runtime.NODEJS_18_X,
code: cdk.aws_lambda.Code.fromInline(`
exports.greeting = async function () {
const value = '${props?.lambdaValue ?? "Hello AWS CDK"}';
console.log(value);
${props?.lambdaIsReturn ? "return value;" : ""}
};
`),
handler: "index.greeting",
});
到這邊為止,對 AppStack
的變更就結束了,可以先部署上去,如此一來晚些再變更參數時會有比較乾淨的差異性更新可以觀察。
接著,讓我們來去給 AppStack
不同的參數,開啟 bin/app.ts
,並在建立 AppStack
的第三個參數中,加上想要的數值,例如:
{
s3BucketExpiration: cdk.Duration.days(30),
ec2Arch: "arm64",
lambdaValue: "Value from the outside",
lambdaIsReturn: true,
}
然後,看一下是不是有成功的傳入。
Resources
[~] AWS::S3::Bucket bucket bucket43879C71
└─ [~] LifecycleConfiguration
└─ [~] .Rules:
└─ @@ -1,6 +1,6 @@
[ ] [
[ ] {
[-] "ExpirationInDays": 7,
[+] "ExpirationInDays": 30,
[ ] "Status": "Enabled"
[ ] }
[ ] ]
[~] AWS::Lambda::Function function functionF19B1A04
└─ [~] Code
└─ [~] .ZipFile:
├─ [-]
exports.greeting = async function () {
const value = 'Hello AWS CDK';
console.log(value);
};
└─ [+]
exports.greeting = async function () {
const value = 'Value from the outside';
console.log(value);
return value;
};
太好了,的確如我們所指定的參數一致。
現在讓我們建立多個 AppStack
,直接複製貼上就好了,接著看看現在的 Stack 有哪些。
npm run cdk -- list
哇, AWS CDK CLI 生氣了。
/app/node_modules/constructs/src/construct.ts:428
throw new Error(`There is already a Construct with name '${childName}' in ${typeName}${name.length > 0 ? ' [' + name + ']' : ''}`);
^
Error: There is already a Construct with name 'AppStack' in App
at Node.addChild (/app/node_modules/constructs/src/construct.ts:428:13)
at new Node (/app/node_modules/constructs/src/construct.ts:71:17)
at new Construct (/app/node_modules/constructs/src/construct.ts:480:17)
at new Stack (/app/node_modules/aws-cdk-lib/core/lib/stack.js:1:2457)
at new AppStack (/app/lib/app-stack.ts:14:5)
at Object.<anonymous> (/app/bin/app.ts:28:1)
at Module._compile (node:internal/modules/cjs/loader:1256:14)
at Module.m._compile (/app/node_modules/ts-node/src/index.ts:1618:23)
at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
at Object.require.extensions.<computed> [as .ts] (/app/node_modules/ts-node/src/index.ts:1621:12)
Subprocess exited with error 1
還記得我們之前說的嗎?無所不在的 Construct 會利用人與人的連結,建立起唯一路徑 (path
) 。
也就是說,在同一個 scope
(Construct
) 之下的 id
必須為唯一,不可以重複。
來在後面加個後墜快速處理一下。
new AppStack(app, "AppStack-2", {
s3BucketExpiration: cdk.Duration.days(30),
ec2Arch: "arm64",
lambdaValue: "Value from the outside",
lambdaIsReturn: true,
});
接著再來跑一次剛才的指令,會看到 AWS CDK CLI 列出了我們所建立的兩個 Stack 。
AppStack
AppStack-2
但是在我們要來部署的時候,卻又看到另一個錯誤訊息:
Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`
Stacks: AppStack · AppStack-2
原來是之前我們一直在走捷徑,當 AWS CDK App 只有一個 Stack 的時候,可以不需要指定 Stack ,而現在有多個 Stack ,則需要跟 AWS CDK CLI 說明我們要針對哪些 Stack 操作。
可以一一列舉,像是:
npm run cdk -- deploy AppStack AppStack-2
或是使用星號:
npm run cdk -- deploy AppStack*
也可以全選:
npm run cdk -- deploy --all
平安地把第二個 Stack 部署上 AWS 了,那我們來試著用不同的方式指定參數。
new AppStack(app, "AppStack-2", {
s3BucketExpiration: cdk.Duration.days(30),
ec2Arch: process.arch.startsWith("arm") ? "arm64" : "x86_64",
lambdaValue: `Value from env ${
process.env.AWS_LAMBDA_VALUE
} at ${new Date().toISOString()}`,
lambdaIsReturn: true,
});
上面用了 process
以及 Date
來取用 Node.js 本身提供的資訊,事不宜遲,馬上來看看是不是有正確地取用了。
macOS:
AWS_LAMBDA_VALUE='here i am' npm run cdk -- diff AppStack-2
Windows:
$env:AWS_LAMBDA_VALUE='here i am'
npm run cdk -- diff AppStack-2
不只是環境變數,連時間日期與機器架構都有拿到:
Parameters
[-] Parameter SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter: {"Type":"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>","Default":"/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2"}
[+] Parameter SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn2-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter: {"Type":"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>","Default":"/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"}
Resources
[~] AWS::EC2::Instance instance instanceB7CCE687 replace
├─ [~] ImageId (requires replacement)
│ └─ [~] .Ref:
│ ├─ [-] SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter
│ └─ [+] SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter
└─ [~] InstanceType (may cause replacement)
├─ [-] t4g.small
└─ [+] t2.micro
[~] AWS::Lambda::Function function functionF19B1A04
└─ [~] Code
└─ [~] .ZipFile:
├─ [-]
exports.greeting = async function () {
const value = 'Value from the outside';
console.log(value);
return value;
};
└─ [+]
exports.greeting = async function () {
const value = 'Value from env here i am at 2023-09-07T08:00:02.695Z';
console.log(value);
return value;
};
只是取用 Node.js 的資訊肯定無法滿足日益複雜的雲端架構,所以我們要來讓取用不同 Stack 的資訊。
首先,將想要取用的屬性建立出來:
export class AppStack extends cdk.Stack {
public readonly lambdaFunctionName: string;
constructor(scope: Construct, id: string, props?: AppStackProps) {
再來,將值指定進去,這邊我們使用 AWS Lambda 函數名稱,因為我們在建立AWS Lambda 函數時沒有指定,所以他會是由 AWS CDK 與 AWS CloudFormation 的亂數組合而成:
const func = new cdk.aws_lambda.Function(this, "function", {
vpc,
runtime: cdk.aws_lambda.Runtime.NODEJS_18_X,
code: cdk.aws_lambda.Code.fromInline(`
exports.greeting = async function () {
const value = '${props?.lambdaValue ?? "Hello AWS CDK"}';
console.log(value);
${props?.lambdaIsReturn ? "return value;" : ""}
};
`),
handler: "index.greeting",
});
this.lambdaFunctionName = func.functionName;
接著,把我們要參照的 AppStack
存成變數:
const appStack = new AppStack(app, "AppStack", {
最後,直接使用就可以了:
new AppStack(app, "AppStack-2", {
s3BucketExpiration: cdk.Duration.days(30),
ec2Arch: process.arch.startsWith("arm") ? "arm64" : "x86_64",
lambdaValue: `Value from env ${
process.env.AWS_LAMBDA_VALUE
} at ${new Date().toISOString()} and AWS Lambda Function from AppStack is ${
appStack.lambdaFunctionName
}`,
lambdaIsReturn: true,
});
來看一下現在 AWS CDK 跟 AWS 的差異,是不是數值都上去了呢?
Stack AppStack
Outputs
[+] Output Exports/Output{"Ref":"functionF19B1A04"} ExportsOutputReffunctionF19B1A04ABA385C1: {"Value":{"Ref":"functionF19B1A04"},"Export":{"Name":"AppStack:ExportsOutputReffunctionF19B1A04ABA385C1"}}
Stack AppStack-2
Resources
[~] AWS::Lambda::Function function functionF19B1A04
└─ [~] Code
└─ [~] .ZipFile:
└─ @@ -1,1 +1,12 @@
[-] "\n exports.greeting = async function () {\n const value = 'Value from env here i am at 2023-09-07T08:00:02.695Z';\n console.log(value);\n return value;\n };\n "
[+] {
[+] "Fn::Join": [
[+] "",
[+] [
[+] "\n exports.greeting = async function () {\n const value = 'Value from env here i am at 2023-09-07T08:00:05.654Z and AWS Lambda Function from AppStack is ",
[+] {
[+] "Fn::ImportValue": "AppStack:ExportsOutputReffunctionF19B1A04ABA385C1"
[+] },
[+] "';\n console.log(value);\n return value;\n };\n "
[+] ]
[+] ]
[+] }
怎麼原本的 AppStack
多了一個資源?
[+] Output Exports/Output{"Ref":"functionF19B1A04"} ExportsOutputReffunctionF19B1A04ABA385C1: {"Value":{"Ref":"functionF19B1A04"},"Export":{"Name":"AppStack:ExportsOutputReffunctionF19B1A04ABA385C1"}}
而且,原本應該要出現 AWS Lambda 函數名稱的地方,怎麼會是這個詭異的字串呢?
[+] {
[+] "Fn::ImportValue": "AppStack:ExportsOutputReffunctionF19B1A04ABA385C1"
[+] },
原來是 AWS CDK 幫忙做的處理。
在 AWS CloudFormation 中,如果要做跨 Stack 的參照,就必須要把想要參照的對象產成輸出 (output) ,而 AWS CDK 已經自動地幫忙做完這件事情了。
我們成功的將 Stack 給予不同的參數讓他可以被重複利用,也學會了如何將不同 Stack 中的動態值做參照,既然開啟了增加複雜度的可能性,接下來就試著編寫 AWS CDK 的測試。